<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>音频律动外凸波峰圆环（带详细注释）</title>
<style>
  html,body{margin:0;background:#111;height:100%;display:flex;justify-content:center;align-items:center;flex-direction:column}
  canvas{border-radius:8px;margin-bottom:12px}
  button{padding:8px 20px;font-size:16px;border:none;border-radius:4px;background:#0ff;color:#111;cursor:pointer}
</style>
</head>
<body>

<!-- 1. 音频标签：src 换成你的 mp3 -->
<audio id="audio" src="1.mp3" crossorigin="anonymous"></audio>
<button id="playBtn">播放 / 暂停</button>

<!-- 2. 画布 -->
<canvas id="c" width="600" height="600"></canvas>

<script>
/* ===========================================================
   一、单个外凸波峰类
   =========================================================== */
class Bump {
  /**
   * @param {number} center  波峰中心的角度（弧度）
   * @param {number} amp     最大振幅（外凸高度）
   * @param {number} width   波峰底宽（弧度）
   * @param {number} cycles  起伏动画完整播放的次数
   */
  constructor(center, amp, width, cycles) {
    this.center = center;     // 在圆环上的位置
    this.amp    = amp;        // 最大高度
    this.width  = width;      // 底宽
    this.maxCycles = cycles;  // 总起伏次数
    this.cycles = cycles;     // 剩余次数
    this.phase  = 0;          // 当前相位，用于 cos 动画
  }

  /* 计算当前帧振幅（随生命周期递减）*/
  currentAmp() {
    // Math.abs 保证永远 ≥ 0 → 永远外凸
    return this.amp * Math.abs(Math.cos(this.phase)) * (this.cycles / this.maxCycles);
  }

  /* 每帧更新，返回 true 表示已完成 */
  update() {
    this.phase += 0.15;                // 速度
    if (this.phase >= Math.PI * 2) {   // 一个完整起伏完成
      this.phase = 0;
      this.cycles--;
    }
    return this.cycles <= 0;           // 生命周期结束
  }
}

/* ===========================================================
   二、圆环波管理器
   =========================================================== */
class RingWave {
  constructor(cx, cy, baseR) {
    this.cx = cx;      // 圆心 x
    this.cy = cy;      // 圆心 y
    this.baseR = baseR;// 圆环基础半径
    this.bumps = [];   // 存放所有 Bump 实例
  }

  /* 手动添加一个波峰（供外部调用） */
  addBump(center, amp, width, cycles) {
    this.bumps.push(new Bump(center, amp, width, cycles));
  }

  /* 绘制 + 生命周期更新 */
  updateAndDraw(ctx) {
    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    ctx.beginPath();
    const pts = 360;
    for (let i = 0; i <= pts; i++) {
      const angle = (i / pts) * Math.PI * 2;
      let pulse = 0;

      /* 叠加所有 Bump 的贡献 */
      this.bumps.forEach(b => {
        const dist = Math.abs(angle - b.center);
        const wrap = Math.min(dist, Math.PI * 2 - dist); // 圆周最短距离
        if (wrap < b.width) {
          const shape = Math.abs(Math.cos(wrap / b.width * Math.PI / 2)); // 外凸形状
          pulse += shape * b.currentAmp();
        }
      });

      const r = this.baseR + pulse;
      const x = this.cx + Math.cos(angle) * r;
      const y = this.cy + Math.sin(angle) * r;
      i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
    }
    ctx.closePath();
    ctx.strokeStyle = '#0ff';
    ctx.lineWidth = 2.5;
    ctx.stroke();
    ctx.fillStyle = 'rgba(0,255,255,0.08)';
    ctx.fill();

    /* 移除已完成的波峰 */
    this.bumps = this.bumps.filter(b => !b.update());
  }
}

/* ===========================================================
   三、Web Audio API 初始化
   =========================================================== */
let analyser, dataArray;
async function initAudio() {
  const audio = document.getElementById('audio');
  const actx  = new (window.AudioContext || window.webkitAudioContext)();
  const src   = actx.createMediaElementSource(audio);
  analyser    = actx.createAnalyser();
  analyser.fftSize = 256;                       // 频域点数
  dataArray   = new Uint8Array(analyser.frequencyBinCount);

  src.connect(analyser);
  analyser.connect(actx.destination);

  /* 绑定按钮事件 */
  document.getElementById('playBtn').onclick = () => {
    if (actx.state === 'suspended') actx.resume();
    audio.paused ? audio.play() : audio.pause();
  };
}
initAudio();

/* ===========================================================
   四、主程序：根据实时音频数据动态添加波峰
   =========================================================== */
const cvs = document.getElementById('c');
const ctx = cvs.getContext('2d');
const ring = new RingWave(300, 300, 150);

/* 读取音频能量 → 生成随机波峰 */
function audioLoop() {
  if (!analyser) return;
  analyser.getByteFrequencyData(dataArray);

  /* 简单三段能量平均 */
  const low  = dataArray.slice(0, 8).reduce((a, b) => a + b, 0) / 8;
  const mid  = dataArray.slice(8, 32).reduce((a, b) => a + b, 0) / 24;
  const high = dataArray.slice(32, 64).reduce((a, b) => a + b, 0) / 32;
  const vol  = Math.min(1, (low + mid + high) / 3 / 255);

  /* 能量阈值 + 数量上限 */
  if (vol > 0.15 && ring.bumps.length < 15) {
    const count = 1 + Math.floor(Math.random() * 3); // 1~3 个
    for (let i = 0; i < count; i++) {
      /* ===== 每次波峰随机给值的核心代码 ===== */
      const center  = Math.random() * Math.PI * 2;      // 随机角度
      const amp     = 10 + vol * 60;                    // 振幅随音量
      const width   = 0.05 + Math.random() * 0.1;        // 随机底宽
      const cycles  = 1; // 随机起伏次数
      ring.addBump(center, amp, width, cycles);
    }
  }
}

/* 主循环 */
function loop() {
  audioLoop();          // 读取音频并添加波峰
  ring.updateAndDraw(ctx);
  requestAnimationFrame(loop);
}
loop();
</script>
</body>
</html>